// https://github.com/Hexer10/youtube_explode_dart/issues/349#issuecomment-2965582545

// ignore_for_file: public_member_api_docs, unused_local_variable, empty_catches, avoid_dynamic_calls

import 'dart:convert';
import 'package:http/http.dart' as http;

// Data Models
class InnerTubeContext {
  const InnerTubeContext({required this.client});
  final InnerTubeClient client;

  Map<String, dynamic> toJson() => {
        'client': client.toJson(),
      };
}

class InnerTubeClient {
  const InnerTubeClient({
    required this.hl,
    required this.gl,
    required this.clientName,
    required this.clientVersion,
  });
  final String hl;
  final String gl;
  final String clientName;
  final String clientVersion;

  Map<String, dynamic> toJson() => {
        'hl': hl,
        'gl': gl,
        'clientName': clientName,
        'clientVersion': clientVersion,
      };
}

class CaptionTrack {
  const CaptionTrack({
    required this.url,
    required this.languageCode,
    required this.isAutoGenerated,
    required this.isTranslatable,
  });

  factory CaptionTrack.fromJson(Map<String, dynamic> json) {
    return CaptionTrack(
      url: (json['baseUrl'] as String).replaceAll('&fmt=srv3', ''),
      languageCode: json['languageCode'] as String,
      isAutoGenerated: json['kind'] == 'asr',
      isTranslatable: json['isTranslatable'] ?? false,
    );
  }
  final String url;
  final String languageCode;
  final bool isAutoGenerated;
  final bool isTranslatable;
}

class Caption {
  const Caption({
    required this.start,
    required this.duration,
    required this.text,
  });
  final double start;
  final double duration;
  final String text;

  double get end => start + duration;

  String get startVtt {
    final duration = Duration(milliseconds: (start * 1000).toInt());
    return getDurationVtt(duration);
  }

  String get endVtt {
    final duration = Duration(milliseconds: (end * 1000).toInt());
    return getDurationVtt(duration);
  }

  String get vtt {
    return '''

$startVtt --> $endVtt
$text
''';
  }

  static String getDurationVtt(Duration duration) {
    String twoDigits(String n) => n.padLeft(2, '0');
    String threeDigits(String n) => n.padLeft(3, '0');

    String hours = duration.inHours.toString();
    String mins = duration.inMinutes.remainder(60).toString();
    String secs = duration.inSeconds.remainder(60).toString();
    String millis = duration.inMilliseconds.remainder(1000).toString();

    String padMins = twoDigits(mins);
    String padSecs = twoDigits(secs);
    String padMillis = threeDigits(millis);

    return '$hours:$padMins:$padSecs:$padMillis';
  }
}

class PlayabilityStatus {
  const PlayabilityStatus({
    required this.status,
    this.reason,
  });

  factory PlayabilityStatus.fromJson(Map<String, dynamic> json) {
    return PlayabilityStatus(
      status: json['status'] as String,
      reason: json['reason'] as String?,
    );
  }
  final String status;
  final String? reason;

  bool get isOk => status == 'OK';
}

class InnerTubeResponse {
  const InnerTubeResponse({
    this.playabilityStatus,
    this.captions,
  });

  factory InnerTubeResponse.fromJson(Map<String, dynamic> json) {
    return InnerTubeResponse(
      playabilityStatus: json['playabilityStatus'] != null
          ? PlayabilityStatus.fromJson(json['playabilityStatus'])
          : null,
      captions: json['captions'] as Map<String, dynamic>?,
    );
  }
  final PlayabilityStatus? playabilityStatus;
  final Map<String, dynamic>? captions;
}

// Exceptions
class YouTubeTranscriptException implements Exception {
  const YouTubeTranscriptException(this.message);
  final String message;

  @override
  String toString() => 'YouTubeTranscriptException: $message';
}

class VideoNotAvailableException extends YouTubeTranscriptException {
  const VideoNotAvailableException(String reason)
      : super('Video unavailable: $reason');
}

class CaptionsNotFoundException extends YouTubeTranscriptException {
  const CaptionsNotFoundException(String videoId)
      : super('No captions found for video: $videoId');
}

class TranscriptsDisabledException extends YouTubeTranscriptException {
  const TranscriptsDisabledException(String videoId)
      : super('Transcripts are disabled for video: $videoId');
}

class LanguageNotFoundException extends YouTubeTranscriptException {
  const LanguageNotFoundException(String languageCode)
      : super('No captions found for language: $languageCode');
}

class ApiKeyExtractionException extends YouTubeTranscriptException {
  const ApiKeyExtractionException(String videoId)
      : super('Failed to extract API key for video: $videoId');
}

class IpBlockedException extends YouTubeTranscriptException {
  const IpBlockedException(String videoId)
      : super('IP blocked by YouTube for video: $videoId');
}

// Main Class
class YouTubeTranscriptFetcher {
  YouTubeTranscriptFetcher({http.Client? httpClient})
      : _httpClient = httpClient ?? http.Client();
  static const String _watchUrl = 'https://www.youtube.com/watch?v=';
  static const String _innertubeApiUrl =
      'https://www.youtube.com/youtubei/v1/player?key=';

  static const InnerTubeContext _innertubeContext = InnerTubeContext(
    client: InnerTubeClient(
      hl: 'en',
      gl: 'US',
      clientName: 'WEB',
      clientVersion: '2.20210721.00.00',
    ),
  );

  final http.Client _httpClient;

  /// Extracts video ID from YouTube URL
  String _extractVideoId(String url) {
    final patterns = [
      RegExp(
          r'(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)'),
      RegExp(r'youtube\.com\/watch\?.*&v=([^&\n?#]+)'),
    ];

    for (final pattern in patterns) {
      final match = pattern.firstMatch(url);
      if (match != null && match.groupCount >= 1) {
        return match.group(1)!;
      }
    }

    // If no pattern matches, assume the input is already a video ID
    if (!url.contains('/') && !url.contains('youtube')) {
      return url;
    }

    throw YouTubeTranscriptException('Invalid YouTube URL or video ID: $url');
  }

  /// Fetches the raw caption data (XML) for a YouTube video
  Future<String> fetchCaptions(String videoId, {String? languageCode}) async {
    // Step 1: Fetch the video page HTML
    final html = await _fetchVideoHtml(videoId);

    // Step 2: Extract the API key from the HTML
    final apiKey = _extractApiKey(html, videoId);

    // Step 3: Fetch InnerTube data
    final innertubeResponse = await _fetchInnertubeData(videoId, apiKey);

    // Step 4: Extract caption tracks
    final captionTracks = _extractCaptionTracks(innertubeResponse, videoId);

    // Step 5: Find the desired caption track
    final captionUrl = _findCaptionUrl(captionTracks, languageCode);

    // Step 6: Fetch the actual caption XML
    final captionXml = await _fetchCaptionXml(captionUrl);

    return captionXml;
  }

  /// Fetches available caption tracks metadata
  Future<List<CaptionTrack>> fetchAvailableCaptions(String videoUrl) async {
    try {
      final videoId = _extractVideoId(videoUrl);

      // Fetch video page and extract data
      final html = await _fetchVideoHtml(videoId);
      final apiKey = _extractApiKey(html, videoId);
      final innertubeResponse = await _fetchInnertubeData(videoId, apiKey);

      return _extractCaptionTracks(innertubeResponse, videoId);
    } catch (e) {
      throw YouTubeTranscriptException(
          'Failed to fetch available captions: $e');
    }
  }

  Future<String> _fetchVideoHtml(String videoId) async {
    final response = await _httpClient.get(
      Uri.parse('$_watchUrl$videoId'),
      headers: {
        'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept-Language': 'en-US,en;q=0.9',
      },
    );

    if (response.statusCode != 200) {
      throw YouTubeTranscriptException(
          'Failed to fetch video page: ${response.statusCode}');
    }

    return response.body;
  }

  String _extractApiKey(String html, String videoId) {
    final pattern = RegExp(r'"INNERTUBE_API_KEY":\s*"([a-zA-Z0-9_-]+)"');
    final match = pattern.firstMatch(html);

    if (match != null && match.groupCount >= 1) {
      return match.group(1)!;
    }

    // Check for common error conditions
    if (html.contains('class="g-recaptcha"')) {
      throw IpBlockedException(videoId);
    }

    throw ApiKeyExtractionException(videoId);
  }

  Future<InnerTubeResponse> _fetchInnertubeData(
      String videoId, String apiKey) async {
    final response = await _httpClient.post(
      Uri.parse('$_innertubeApiUrl$apiKey'),
      headers: {
        'Content-Type': 'application/json',
        'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      },
      body: jsonEncode({
        'context': _innertubeContext.toJson(),
        'videoId': videoId,
      }),
    );

    if (response.statusCode != 200) {
      throw YouTubeTranscriptException(
          'Failed to fetch InnerTube data: ${response.statusCode}');
    }

    final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
    return InnerTubeResponse.fromJson(jsonData);
  }

  List<CaptionTrack> _extractCaptionTracks(
      InnerTubeResponse innertubeResponse, String videoId) {
    // Check playability status
    if (innertubeResponse.playabilityStatus != null) {
      final status = innertubeResponse.playabilityStatus!;
      if (!status.isOk) {
        throw VideoNotAvailableException(status.reason ?? 'Unknown error');
      }
    }

    // Extract caption tracks
    final captions = innertubeResponse.captions;
    if (captions == null) {
      throw CaptionsNotFoundException(videoId);
    }

    final trackList = captions['playerCaptionsTracklistRenderer'];
    if (trackList == null || trackList['captionTracks'] == null) {
      throw TranscriptsDisabledException(videoId);
    }

    final captionTracksJson =
        List<Map<String, dynamic>>.from(trackList['captionTracks']);

    return captionTracksJson.map(CaptionTrack.fromJson).toList();
  }

  String _findCaptionUrl(
      List<CaptionTrack> captionTracks, String? languageCode) {
    if (captionTracks.isEmpty) {
      throw const YouTubeTranscriptException('No caption tracks available');
    }

    // If no language specified, return the first available
    if (languageCode == null || languageCode.isEmpty) {
      return captionTracks.first.url;
    }

    // Try to find exact language match
    for (final track in captionTracks) {
      if (track.languageCode == languageCode) {
        return track.url;
      }
    }

    // Try to find partial language match (e.g., 'en' matches 'en-US')
    for (final track in captionTracks) {
      if (track.languageCode.startsWith(languageCode)) {
        return track.url;
      }
    }

    throw LanguageNotFoundException(languageCode);
  }

  Future<String> _fetchCaptionXml(String captionUrl) async {
    final response = await _httpClient.get(
      Uri.parse(captionUrl),
      headers: {
        'User-Agent':
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
      },
    );

    if (response.statusCode != 200) {
      throw YouTubeTranscriptException(
          'Failed to fetch caption XML: ${response.statusCode}');
    }

    return response.body;
  }

  void dispose() {
    _httpClient.close();
  }
}

// Caption Parser
class CaptionParser {
  static List<Caption> parseXml(String xml) {
    final captions = <Caption>[];

    // Basic XML parsing using regex (consider using xml package for production)
    final textPattern =
        RegExp(r'<text start="([\d.]+)" dur="([\d.]+)"[^>]*>(.*?)</text>');
    final matches = textPattern.allMatches(xml);

    for (final match in matches) {
      captions.add(Caption(
        start: double.parse(match.group(1)!),
        duration: double.parse(match.group(2)!),
        text: _decodeHtml(match.group(3)!),
      ));
    }

    return captions;
  }

  static String _decodeHtml(String text) {
    return text
        .replaceAll('&amp;', '&')
        .replaceAll('&lt;', '<')
        .replaceAll('&gt;', '>')
        .replaceAll('&quot;', '"')
        .replaceAll('&#39;', "'")
        .replaceAll(RegExp('<[^>]+>'), ''); // Remove HTML tags
  }
}

// Example usage
void main() async {
  final fetcher = YouTubeTranscriptFetcher();

  try {
    // Example 1: Fetch raw caption XML
    final captionXml = await fetcher.fetchCaptions(
      'https://www.youtube.com/watch?v=Wr2crpug1j4',
      languageCode: 'en',
    );

    // Example 2: Get available captions metadata
    final availableCaptions = await fetcher.fetchAvailableCaptions(
      'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
    );
    for (final caption in availableCaptions) {}

    // Example 3: Parse captions to structured data
    final parsedCaptions = CaptionParser.parseXml(captionXml);
    for (final caption in parsedCaptions.take(3)) {}
    // ignore: unused_catch_clause
  } on YouTubeTranscriptException catch (e) {
  } catch (e) {
  } finally {
    fetcher.dispose();
  }
}
